package cmd

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strings"
	"time"

	piperDocker "github.com/SAP/jenkins-library/pkg/docker"
	piperhttp "github.com/SAP/jenkins-library/pkg/http"
	"github.com/SAP/jenkins-library/pkg/log"
	"github.com/SAP/jenkins-library/pkg/malwarescan"
	"github.com/SAP/jenkins-library/pkg/piperutils"
	"github.com/SAP/jenkins-library/pkg/telemetry"
	"github.com/SAP/jenkins-library/pkg/toolrecord"
	"github.com/pkg/errors"
)

type malwareScanUtils interface {
	OpenFile(name string, flag int, perm os.FileMode) (io.ReadCloser, error)
	SHA256(path string) (string, error)

	newDockerClient(piperDocker.ClientOptions) piperDocker.Download

	malwarescan.Client
	piperutils.FileUtils
}

type malwareScanUtilsBundle struct {
	malwarescan.Client
	*piperutils.Files
}

func (utils *malwareScanUtilsBundle) OpenFile(name string, flag int, perm os.FileMode) (io.ReadCloser, error) {
	return utils.Files.FileOpen(name, flag, perm)
}

func (utils *malwareScanUtilsBundle) newDockerClient(options piperDocker.ClientOptions) piperDocker.Download {
	dClient := piperDocker.Client{}
	dClient.SetOptions(options)
	return &dClient
}

func newMalwareScanUtilsBundle(config malwareExecuteScanOptions) *malwareScanUtilsBundle {
	timeout, err := time.ParseDuration(fmt.Sprintf("%ss", config.Timeout))
	if err != nil {
		timeout = 60
		log.Entry().Warnf("Unable to parse timeout for malwareScan: '%v'. Falling back to %ds", err, timeout)
	}

	httpClientOptions := piperhttp.ClientOptions{
		Username:           config.Username,
		Password:           config.Password,
		MaxRequestDuration: timeout,
		TransportTimeout:   timeout,
	}

	httpClient := &piperhttp.Client{}
	httpClient.SetOptions(httpClientOptions)

	return &malwareScanUtilsBundle{
		Client: &malwarescan.ClientImpl{
			HTTPClient: httpClient,
			Host:       config.Host,
		},
		Files: &piperutils.Files{},
	}
}

func malwareExecuteScan(config malwareExecuteScanOptions, telemetryData *telemetry.CustomData) {
	utils := newMalwareScanUtilsBundle(config)

	err := runMalwareScan(&config, telemetryData, utils)
	if err != nil {
		log.Entry().WithError(err).Fatal("step execution failed")
	}
}

func runMalwareScan(config *malwareExecuteScanOptions, telemetryData *telemetry.CustomData, utils malwareScanUtils) error {
	file, err := selectAndPrepareFileForMalwareScan(config, utils)
	if err != nil {
		return err
	}

	log.Entry().Infof("Scanning file \"%s\" for malware using service \"%s\"", file, config.Host)

	candidate, err := utils.OpenFile(file, os.O_RDONLY, 0666)
	if err != nil {
		return err
	}
	defer candidate.Close()

	scannerInfo, err := utils.Info()

	log.Entry().Infof("***************************************")
	log.Entry().Infof("* Engine:     %s", scannerInfo.EngineVersion)
	log.Entry().Infof("* Signatures: %s", scannerInfo.SignatureTimestamp)
	log.Entry().Infof("***************************************")

	if _, err = createToolRecordMalwareScan(utils, "./", config, scannerInfo); err != nil {
		return err
	}

	scanResponse, err := utils.Scan(candidate)

	if err != nil {
		return err
	}

	if err = createMalwareScanReport(config, scanResponse, utils); err != nil {
		return err
	}

	log.Entry().Debugf(
		"File '%s' has been scanned. MalwareDetected: %t, EncryptedContentDetected: %t, ScanSize: %d, MimeType: '%s', SHA256: '%s', Finding: '%s'",
		file,
		scanResponse.MalwareDetected,
		scanResponse.EncryptedContentDetected,
		scanResponse.ScanSize,
		scanResponse.MimeType,
		scanResponse.SHA256,
		scanResponse.Finding)

	if err = validateHash(scanResponse.SHA256, file, utils); err != nil {
		return err
	}

	if scanResponse.MalwareDetected || scanResponse.EncryptedContentDetected {
		return fmt.Errorf("Malware scan failed for file '%s'. Malware detected: %t, encrypted content detected: %t, finding: %v",
			file, scanResponse.MalwareDetected, scanResponse.EncryptedContentDetected, scanResponse.Finding)
	}

	log.Entry().Infof("Malware scan succeeded for file '%s'. Malware detected: %t, encrypted content detected: %t",
		file, scanResponse.MalwareDetected, scanResponse.EncryptedContentDetected)

	return nil
}

func selectAndPrepareFileForMalwareScan(config *malwareExecuteScanOptions, utils malwareScanUtils) (string, error) {
	if len(config.ScanFile) > 0 {
		return config.ScanFile, nil
	}

	// automatically detect the file to be scanned depending on the buildtool
	if len(config.ScanImage) > 0 {
		saveImageOptions := containerSaveImageOptions{
			ContainerImage:            config.ScanImage,
			ContainerRegistryURL:      config.ScanImageRegistryURL,
			ContainerRegistryUser:     config.ContainerRegistryUser,
			ContainerRegistryPassword: config.ContainerRegistryPassword,
			DockerConfigJSON:          config.DockerConfigJSON,
			ImageFormat:               "tarball",
		}

		dClientOptions := piperDocker.ClientOptions{ImageName: saveImageOptions.ContainerImage, RegistryURL: saveImageOptions.ContainerRegistryURL, LocalPath: "", ImageFormat: saveImageOptions.ImageFormat}
		dClient := utils.newDockerClient(dClientOptions)

		tarFile, err := runContainerSaveImage(&saveImageOptions, &telemetry.CustomData{}, "./cache", "", dClient, utils)

		if err != nil {
			if strings.Contains(fmt.Sprint(err), "no image found") {
				log.SetErrorCategory(log.ErrorConfiguration)
			}
			return "", errors.Wrapf(err, "failed to download Docker image %v", config.ScanImage)
		}
		return tarFile, nil
	}

	return "", fmt.Errorf("Please specify a file to be scanned")
}

func validateHash(remoteHash, fileName string, utils malwareScanUtils) error {
	hash, err := utils.SHA256(fileName)
	if err != nil {
		return err
	}

	if hash == remoteHash {
		log.Entry().Infof("Hash returned from malwarescan service matches file hash for file '%s' (%s)", fileName, hash)
	} else {
		return fmt.Errorf("Hash returned from malwarescan service ('%s') does not match file hash ('%s') for file '%s'",
			remoteHash, hash, fileName)
	}

	return nil
}

// create toolrecord file for malwarescan
func createToolRecordMalwareScan(utils malwareScanUtils, workspace string, config *malwareExecuteScanOptions, scanner *malwarescan.Info) (string, error) {
	record := toolrecord.New(utils, workspace, "malwarescan", config.Host)
	record.SetOverallDisplayData("Malware Scanner", "")

	if err := record.AddKeyData("engineVersion", scanner.EngineVersion, "Engine Version", ""); err != nil {
		return "", err
	}

	if err := record.AddKeyData("signatureTimestamp", scanner.SignatureTimestamp, "Signature Timestamp", ""); err != nil {
		return "", err
	}

	if err := record.Persist(); err != nil {
		return "", err
	}

	return record.GetFileName(), nil
}

func createMalwareScanReport(config *malwareExecuteScanOptions, scanResult *malwarescan.ScanResult, utils malwareScanUtils) error {
	scanResultJSON, err := json.Marshal(scanResult)

	if err != nil {
		return err
	}

	return utils.FileWrite(config.ReportFileName, scanResultJSON, 0666)
}
